# 5.3 异步Http Client

支持协程的异步Http Client很关键,在微服务系统框架中,服务与服务的交互大多是通过Http接口来实现,即使有封装RPC,也大多在Http Client的基础上,当然我们也可以选择自定义的Tcp文本或者二进制协议,这里我们主要介绍MSF框架中的Http Client的实现与使用。本节中的示例代码:[https://github.com/pinguo/php-msf-demo/app/Controllers/Http.php](https://github.com/pinguo/php-msf-demo/blob/master/app/Controllers/Http.php)

## 实现

框架对http client的支持是基于swoole_http_client,同时在此基础上封装了dns查询、简单快捷操作、多个请求并行的各种方法。

## 基本用法

```php
<?php
/**
 * 异步HTTP CLIENT示例
 *
 * @author camera360_server@camera360.com
 * @copyright Chengdu pinguo Technology Co.,Ltd.
 */

namespace App\Controllers;

use PG\MSF\Controllers\Controller;
use \PG\MSF\Client\Http\Client;

class Http extends Controller
{
    /**
     * 获取百度首页,手工进行DNS解析和数据拉取
     */
    public function actionBaiduIndexWithOutDNS()
    {
        /**
         * @var Client $client
         */
        $client  = $this->getObject(Client::class);
        yield $client->goDnsLookup('http://www.baidu.com');

        $sendGet = $client->goGet('/');
        $result  = yield $sendGet;

        $this->outputView(['html' => $result['body']]);
    }
}
```

这种用法将一个Http请求方法分成为两步: 第一步，DNS查询；第二步，Get请求。可能大家会很奇怪,不就一个Http请求嘛,还分两步?其实我们原来使用CURL扩展的时候,也是这两个步骤,只是CURL内部把我们完成了DNS查询。

另外,由于DNS查询是一次UDP的请求,PHP内置函数`string gethostbyname ( string $hostname )`是同步阻塞模式,如果使用这个函数,将使我们的Sever退化为同步Server,MSF框架进行DNS查询使用了`swoole_async_dns_lookup()`进行异步DNS解析。

一个http请求,开发代码进行两次yield,开发效率不高,但是性能是最好的;同时我们也提供一些快捷的方法,在只有一次接口请求的开发中性能和效率均可得到提升。

## 快捷POST/GET

```php
<?php
/**
 * 异步HTTP CLIENT示例
 *
 * @author camera360_server@camera360.com
 * @copyright Chengdu pinguo Technology Co.,Ltd.
 */

namespace App\Controllers;

use PG\MSF\Controllers\Controller;
use \PG\MSF\Client\Http\Client;

class Http extends Controller
{
    /**
     * 获取百度首页,自动进行DNS,自动通过Get拉取数据
     */
    public function actionBaiduIndexGet()
    {
        /**
         * @var Client $client
         */
        $client  = $this->getObject(Client::class);
        $result = yield $client->goSingleGet('http://www.baidu.com/');

        $this->outputView(['html' => $result['body']]);
    }

    /**
     * 获取百度首页,自动进行DNS,自动通过Post拉取数据
     */
    public function actionBaiduIndexPost()
    {
        /**
         * @var Client $client
         */
        $client  = $this->getObject(Client::class);
        $result = yield $client->goSinglePost('http://www.baidu.com/');

        $this->outputView(['html' => $result['body']]);
    }
}

```

`\PG\MSF\Client\Http\Client::goSingleGet()`,`\PG\MSF\Client\Http\Client::goSinglePost()`两个方法可以快捷的自动完成DNS和请求的发送,直接返回响应内容。

## 响应数据

```
[
    'errCode' => 0
    'sock' => 17
    'host' => '180.97.33.107'
    'port' => 80
    'headers' => [
        'content-type' => 'text/html'
        'content-encoding' => 'gzip'
        'cache-control' => 'no-cache'
        'pragma' => 'no-cache'
        'content-length' => '363'
        'set-cookie' => 'bai=16.;Domain=.baidu.com;Path=/;Max-Age=10'
    ]
    'type' => 1025
    'requestHeaders' => [
        'Host' => 'www.baidu.com'
        'X-Ngx-LogId' => '59496e12c3474d040f41fda2'
    ]
    'requestBody' => null
    'cookies' => [
        'bai' => '16.'
    ]
    'set_cookie_headers' => [
        'bai' => 'bai=16.;Domain=.baidu.com;Path=/;Max-Age=10'
    ]
    'body' => '<body></body><script type=\"text/javascript\">u=\"https://www.baidu.com/?tn=93817326_hao_pg\";d=document;/webkit/i.test(navigator.userAgent)?(f=d.createElement(\'iframe\'),f.style.width=1,f.style.height=1,f.frameBorder=0,d.body.appendChild(f).src=\'javascript:\"<script>top.location.replace(\\\'\'+u+\'\\\')<\\/script>\"\'):(d.open(),d.write([\'<meta http-equiv=\"refresh\"content=\"0;url=\',\'\"/>\'].join(u)),d.close());function g(k){return v=eval(\"/\"+k+\"=(.*?)(&|$)/i.exec(location.href)\"),v?v[1]:\"\"}</script>'
    'statusCode' => 200
]
```

其中:

errCode的具体含义:[附录：Linux错误信息(errno)列表](https://wiki.swoole.com/wiki/page/172.html)

statusCode为Http响应的状态码:

[维基百科HTTP状态码](https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81)

[OSCHINA HTTP状态码](http://tool.oschina.net/commons?type=5)

body为响应正文

## 并行请求

Http请求分成了DNS查询和发送数据两个异步部分,从而在多个内部接口请求中会写大量的冗余代码的,故框架封装了简单实用的并行的Http Client,大大的简化了发送并行请求。

```php
<?php
/**
 * 异步HTTP CLIENT示例
 *
 * @author camera360_server@camera360.com
 * @copyright Chengdu pinguo Technology Co.,Ltd.
 */

namespace App\Controllers;

use PG\MSF\Controllers\Controller;
use \PG\MSF\Client\Http\Client;

class Http extends Controller
{
    // 略
    /**
     * 并行多次获取百度首页,自动进行DNS,自动通过Get或者Post拉取数据
     */
    public function actionConcurrentBaiduIndex()
    {
        /**
         * @var Client $client
         */
        $client   = $this->getObject(Client::class);
        $requests = [
            'http://www.baidu.com/',
            [
                'url'    => 'http://www.baidu.com/',
                'method' => 'POST'
            ],
        ];

        $results = yield $client->goConcurrent($requests);

        $this->outputView(['html' => $results[0]['body'] . $results[0]['body']]);
    }
}
```

`\PG\MSF\Client\Http\Client::goConcurrent($requests)`是我们封装的快捷并行请求方式，`$requests`的数据结构如：

```php
[
    'http://www.baidu.com/xxx',
    [
        // 必须为全路径URL
        'url'         => 'http://www.baidu.com/xxx',
        'method'      => 'GET',
        'dns_timeout' => 1000, // 默认为30s
        'timeout'     => 3000,  // 默认不超时
        'headers'     => [],    // 默认为空
        'data'        => ['a' => 'b'] // 发送数据
    ],
    [
        'url'         => 'http://www.baidu.com/xxx',
        'method'      => 'POST',
        'timeout'     => 3000,
        'headers'     => [],
        'data'        => ['a' => 'b'] // 发送数据
    ],
    [
        'url'         => 'http://www.baidu.com/xxx',
        'method'      => 'POST',
        'timeout'     => 3000,
        'headers'     => [],
        'data'        => ['a' => 'b'] // 发送数据
    ],
]
```

## DNS缓存

HTTP Client的DNS查询为提升性能，默认情况下，已经开启缓存，缓存策略为：

1. DNS缓存有效时间默认为60s
2. 已解析DNS使用次数上限为10000次

只要判断有效时间，如果已过有效期即缓存失效；如果在有效期内，使用次数超过10000次，则重新进行DNS解析

## DNS配置

默认开启了DNS缓存，并有相应的策略，我们也提供了配置项来修改缓存策略，

```php
$config['http']['dns'] = [
    // 有效时间,单位秒
    'expire' => 30,
    // 使用次数上限
    'times' => 1000,
];
```

# links
  * [目录](../README.md)
  * 上一节: [类的加载](5.2-类的加载.md)
  * 下一节: [请求上下文](5.4-请求上下文.md)